﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Net.Sockets;
using System.Net;
using OpenGameEngine.Logging;

namespace OpenGameEngine.Net.Client
{
    public class ClientBase
    {
        public event EventHandler<ErrorEventArgs> Error;

        private Exception _exception;

        public Boolean Connected { get { return (Client == null) ? false : Client.Connected; } }

        public delegate void PacketHandler(ClientBase client, IPacket packet);
        private PacketHandler[] PacketHandlerDelegates { get; set; }

        public TcpClient Client { get; set; }
        public Queue<IPacket> SendQueue { get; set; }
        public IPEndPoint EndPoint { get; set; }

        private Thread NetworkWorkerThread { get; set; }

        protected internal GameStream Stream { get; set; }
        protected internal NetworkStream NetworkStream { get; set; }

        private Boolean NetworkWorkerRunning = false;
        private Boolean NetworkWorkerEnded = true;

        public event EventHandler<ClientDisconnectedEventArgs> Disconnected;
        public event EventHandler<PacketReceivedEventArgs> PacketReceived;

        private string _reason = String.Empty;

        public ClientBase()
        {
            PacketHandlerDelegates = new PacketHandler[256];
            SendQueue = new Queue<IPacket>();
        }

        public virtual Boolean Connect(IPEndPoint endPoint)
        {
            try
            {
                if (Client != null && Client.Connected)
                    throw new InvalidOperationException("Already connected to a server!");

                EndPoint = endPoint;
                Client = new TcpClient();
                Client.Connect(EndPoint);
                NetworkStream = Client.GetStream();
                Stream = new GameStream(NetworkStream);

                NetworkWorkerRunning = true;
                NetworkWorkerEnded = false;

                NetworkWorkerThread = new Thread(NetworkWorker);
                NetworkWorkerThread.Name = "Network Worker";
                NetworkWorkerThread.Start();

                return true;
            }
            catch (Exception e)
            {
                OnError(e);
                return false;
            }
        }

        public Exception GetLastException()
        {
            return _exception;
        }

        public virtual void Disconnect(string reason)
        {
            NetworkWorkerRunning = false;
            LogProvider.Log("Info: Waiting for network worker to end!", LogImportance.Low);

            _reason = reason;

            while (!NetworkWorkerEnded)
                Thread.Sleep(1);

            Client.Close();
        }

        public void SendPacket(IPacket packet)
        {
            SendQueue.Enqueue(packet);
        }

        private void NetworkWorker()
        {
            while (NetworkWorkerRunning)
            {
                Thread.Sleep(1);

                while (SendQueue.Count != 0)
                {
                    IPacket packet = SendQueue.Dequeue();

                    if (packet != null)
                    {
                        try
                        {
                            packet.WritePacket(Stream);

                            Stream.Flush();
                        }
                        catch (Exception e)
                        {
                            OnError(e);
                        }
                    }

                    Thread.Sleep(1);
                }

                DateTime readTimeout = DateTime.Now.AddMilliseconds(20);
                while (NetworkStream.DataAvailable && DateTime.Now < readTimeout)
                {
                    IPacket packet = null;
                    try
                    {
                        packet = PacketReader.ReadPacket(Stream);

                        HandlePacket(packet);
                    }
                    catch (Exception e)
                    {
                        OnError(e);
                    }
                }

                if (!this.Connected)
                {
                    this.Disconnect("The TCP stream closed");
                }
            }

            if (Disconnected != null)
                Disconnected(this, new ClientDisconnectedEventArgs((String.IsNullOrEmpty(_reason)) ? ((GetLastException() != null) ? GetLastException().Message : "Network worker stopped working") : _reason));

            NetworkWorkerEnded = true;
            LogProvider.Log("Info: Network Worker exited!", LogImportance.Low);
        }

        protected internal virtual void OnError(Exception e)
        {
            _exception = e;
            LogProvider.Log(e);

            if (Error != null)
                Error(this, new ErrorEventArgs(e));
        }

        public void HandlePacket(IPacket packet)
        {
            if (PacketHandlerDelegates[packet.Id] != null)
            {
                if (PacketReceived != null)
                    PacketReceived(this, new PacketReceivedEventArgs(packet));

                PacketHandlerDelegates[packet.Id](this, packet);
            }
            else
                Logging.LogProvider.Log("Warning: No packet handlers for (0x" + packet.Id.ToString("X2") + ")", LogImportance.Medium);
        }

        public void RegisterPacketHandler(Type packetType, PacketHandler handler)
        {
            IPacket packet = PacketReader.PacketFromType(packetType);
            byte packetId = packet.Id;
            PacketHandlerDelegates[packetId] = handler;
            PacketReader.OverridePacket(packetType);
        }
    }
}
